Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/next/pages/share/public_paths/page/[page].tsx
Views: 687
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45/*6This is simply a list of *all* publicly shared files/directories,7with a simple page. It is mainly meant to be walked by crawlers8such as Google and for people to browse.9*/1011import { useEffect, useState } from "react";12import { Alert, Button, Input, Popconfirm, Radio, Space } from "antd";13import Link from "next/link";14import SiteName from "components/share/site-name";15import getPool, { timeInSeconds } from "@cocalc/database/pool";16import PublicPaths from "components/share/public-paths";17import { Layout } from "components/share/layout";18import withCustomize from "lib/with-customize";19import { Customize } from "lib/share/customize";20import GoogleSearch from "components/share/google-search";21import ProxyInput from "components/share/proxy-input";22import getAccountId from "lib/account/get-account";23import A from "components/misc/A";24import { useRouter } from "next/router";25import useProfile from "lib/hooks/profile";26import apiPost from "lib/api/post";2728const PAGE_SIZE = 100;2930function getPage(obj): number {31let { page } = obj ?? {};32if (page == null) {33return 1;34}35page = parseInt(page);36if (isFinite(page)) {37return Math.max(page, 1);38}39return 1;40}4142function Pager({ page, publicPaths }) {43const router = useRouter();4445return (46<div>47Page {page}48 49{page > 1 ? (50<Link51href={{52pathname: `/share/public_paths/page/${page - 1}`,53query: router.query,54}}55as={`/share/public_paths/page/${page - 1}${56router.asPath.split("?")[1] ? "?" + router.asPath.split("?")[1] : ""57}`}58passHref59>60Previous61</Link>62) : (63<span style={{ color: "#888" }}>Previous</span>64)}65 66{publicPaths != null && publicPaths.length >= PAGE_SIZE ? (67<Link68href={{69pathname: `/share/public_paths/page/${page + 1}`,70query: router.query,71}}72as={`/share/public_paths/page/${page + 1}${73router.asPath.split("?")[1] ? "?" + router.asPath.split("?")[1] : ""74}`}75passHref76>77Next78</Link>79) : (80<span style={{ color: "#888" }}>Next</span>81)}82</div>83);84}8586export default function All({ page, publicPaths, customize }) {87const pager = <Pager page={page} publicPaths={publicPaths} />;88const router = useRouter();89const [sort, setSort] = useState<string>("last_edited");9091// Set default value of `sort` from query parameter `sort`92useEffect(() => {93if (router.query.sort) {94setSort(router.query.sort as string);95}96}, [router.query.sort]);9798function handleSortChange(e) {99const sort = e.target.value;100// Update the query parameter with new `sort` value101router.push({102pathname: router.pathname,103query: { ...router.query, sort },104});105}106107const [search, setSearch] = useState<string>("");108useEffect(() => {109if (router.query.search) {110setSearch(router.query.search as string);111}112}, [router.query.search]);113114function handleSearchGo(search: string) {115router.push({116pathname: router.pathname,117query: { ...router.query, search },118});119}120121return (122<Customize value={customize}>123<Layout title={`Page ${page} of public files`}>124<div>125<Space126style={{127float: "right",128justifyContent: "flex-end",129marginTop: "7.5px",130}}131direction="vertical"132>133<GoogleSearch style={{ width: "450px", maxWidth: "90vw" }} />134</Space>135<h2>136Browse publicly shared documents on <SiteName />137</h2>138<ProxyInput />139Star items to easily <A href="/stars">find them in your list</A>140.141<br />142<br />143<Input.Search144allowClear145placeholder="Search path & description..."146style={{ marginLeft: "5px", float: "right", width: "275px" }}147value={search}148onChange={(e) => {149setSearch(e.target.value);150if (!e.target.value) {151setTimeout(() => {152handleSearchGo("");153}, 1);154}155}}156onSearch={() => handleSearchGo(search)}157onPressEnter={() => handleSearchGo(search)}158/>159<Radio.Group160value={sort}161onChange={handleSortChange}162style={{ float: "right" }}163>164<Radio.Button value="last_edited">Newest</Radio.Button>165<Radio.Button value="-last_edited">Oldest</Radio.Button>166<Radio.Button value="stars">Stars</Radio.Button>167<Radio.Button value="-stars">Least stars</Radio.Button>168<Radio.Button value="views">Views</Radio.Button>169<Radio.Button value="-views">Least views</Radio.Button>170</Radio.Group>171{pager}172<br />173{typeof router.query.search == "string" &&174router.query.search.trim() &&175publicPaths.length > 0 && (176<AdminUnpublish publicPaths={publicPaths} />177)}178<PublicPaths publicPaths={publicPaths} />179<br />180{pager}181</div>182</Layout>183</Customize>184);185}186187async function adminUnpublish(id: string): Promise<void> {188const query = {189crm_public_paths: {190id,191disabled: true,192},193};194await apiPost("/user-query", { query });195}196197function AdminUnpublish({ publicPaths }) {198const profile = useProfile();199const router = useRouter();200const [error, setError] = useState("");201202if (!profile?.is_admin) return null;203204const handleUnpublish = async () => {205setError("");206try {207await Promise.all(publicPaths.map((x) => adminUnpublish(x.id)));208} catch (error) {209setError(error.toString());210}211// refresh the current page212router.push({213pathname: router.pathname,214query: router.query,215});216};217218return (219<Alert220style={{ margin: "0 0 15px" }}221type="info"222message={"Administrator Controls"}223description={224<div>225{error && (226<Alert227showIcon228style={{ margin: "15px 0" }}229message={"Error"}230description={error}231type="error"232closable233onClose={() => setError("")}234/>235)}236<Popconfirm237title={238<div style={{ width: "400px" }}>239Are you sure you want to unpublish ALL {publicPaths.length}{" "}240items displayed below? These items will be made completely241private (not visible in any way, except to collaborators).242</div>243}244onConfirm={handleUnpublish}245okText="Yes"246cancelText="No"247>248<Button danger>249Unpublish ALL {publicPaths.length} listed items...250</Button>251</Popconfirm>252</div>253}254/>255);256}257258export async function getServerSideProps(context) {259const isAuthenticated = (await getAccountId(context.req)) != null;260const page = getPage(context.params);261const sort = getSort(context);262const { search, searchQuery } = getSearch(context);263const pool = getPool("medium");264const params = [isAuthenticated, PAGE_SIZE, PAGE_SIZE * (page - 1)];265if (search) {266params.push(search);267}268const { rows } = await pool.query(269`SELECT public_paths.id, public_paths.path, public_paths.url, public_paths.description, ${timeInSeconds(270"public_paths.last_edited",271"last_edited",272)}, projects.avatar_image_tiny,273counter::INT,274(SELECT COUNT(*)::INT FROM public_path_stars WHERE public_path_id=public_paths.id) AS stars275FROM public_paths, projects276WHERE public_paths.project_id = projects.project_id277AND public_paths.vhost IS NULL AND public_paths.disabled IS NOT TRUE AND public_paths.unlisted IS NOT TRUE AND278public_paths.url IS NULL AND279((public_paths.authenticated IS TRUE AND $1 IS TRUE) OR (public_paths.authenticated IS NOT TRUE))280${searchQuery}281ORDER BY ${sort} LIMIT $2 OFFSET $3`,282params,283);284285return await withCustomize({ context, props: { page, publicPaths: rows } });286}287288function getSearch(context) {289const { query } = context;290const search = query?.search || "";291if (search) {292return {293search: `%${search}%`,294searchQuery:295"AND (LOWER(public_paths.path) LIKE LOWER($4) OR LOWER(public_paths.description) LIKE LOWER($4))",296};297} else {298return { search, searchQuery: "" };299}300}301302function getSort(context) {303switch (context.query?.sort) {304case "stars":305return "stars DESC, public_paths.last_edited DESC";306case "-stars":307return "stars ASC, public_paths.last_edited DESC";308case "views":309return "COALESCE(counter,0) DESC, public_paths.last_edited DESC";310case "-views":311return "COALESCE(counter,0) ASC, public_paths.last_edited DESC";312case "-last_edited":313return "public_paths.last_edited ASC";314default:315return "public_paths.last_edited DESC";316}317}318319320